﻿using System;
using System.Collections.Generic;
using IcicleFramework.Components.Collision;
using IcicleFramework.Entities;
using IcicleFramework.GameServices;
using Microsoft.Xna.Framework;

namespace IcicleFramework.Collision
{
    public class CollisionManager : GameService, ICollisionManager
    {
        protected Dictionary<Guid, OnCollisionHandler> collisionEvents;

        protected QuadTree<ICollisionComponent> collisionTree;

        protected Dictionary<Guid, CollisionDictionary> currentCollisions;

        protected Pool<CollisionDictionary> listPool; 

        protected List<ICollisionComponent> tempList;

        protected Dictionary<Guid, ICollisionComponent> movedLastFrame;

        protected ILayerManager layerManager;

        public bool ResolveCollisions { get; set; }

        public bool Active { get; set; }

        #region Constructors

        public CollisionManager(RectangleF worldSpace)
        {
            listPool = new Pool<CollisionDictionary>(1000, d => !d.Destroyed, () => new CollisionDictionary());
            currentCollisions = new Dictionary<Guid, CollisionDictionary>();
            tempList = new List<ICollisionComponent>(64);
            movedLastFrame = new Dictionary<Guid, ICollisionComponent>(512);
            collisionEvents = new Dictionary<Guid, OnCollisionHandler>();

            collisionTree = new QuadTree<ICollisionComponent>(new BoundingBox2D(worldSpace));
        }

        #endregion

        public override void Initialize()
        {
            IGameObjectFactory factory = GameServiceManager.GetService<IGameObjectFactory>();

            if (factory != null)
                factory.OnGameObjectCreated += OnGameObjectCreated;

            layerManager = GameServiceManager.GetService<ILayerManager>();

            Active = true;
        }



        #region Subscribing and Unsubscribing

        public bool SubscribeCollisionEvent(Guid GUID, CollisionHandler handler)
        {
            return true;
        }

        public bool SubscribeCollisionEvent(Guid GUID, OnCollisionHandler handler)
        {
            bool subscribed = false;

            if (collisionEvents.ContainsKey(GUID))
            {
                collisionEvents[GUID] += handler;
                subscribed = true;
            }

            return subscribed;
        }

        public void UnsubscribeCollisionEvent(Guid GUID, OnCollisionHandler handler)
        {
            if (collisionEvents.ContainsKey(GUID))
            {
                collisionEvents[GUID] -= handler;
            }
        }

        #endregion
        

        #region Internal Event Handlers

        /// <summary>
        /// Fired whenever a moveable object has changed its position. We will test to
        /// ensure that the movement is valid and then tell its QuadTree that it has moved.
        /// </summary>
        /// <param name="sender">The IMoveable object that has moved, triggering this OnMove event.</param>
        private void OnMove(IGameObject sender)
        {
            ICollisionComponent collisionComp = sender.GetComponent<ICollisionComponent>();

            if (Active && collisionComp != null)
            {
                //collisionTree.ObjectMoved(collisionComp);

                //Add this component to the list of components that have moved this frame.
                if (!movedLastFrame.ContainsKey(sender.GUID))
                {
                    movedLastFrame.Add(sender.GUID, collisionComp);
                }
            }
        }

        private void OnGameObjectCreated(IGameObject newObject)
        {
            if (newObject.GetComponent<ICollisionComponent>() != null)
                RegisterObject(newObject);
        }

        private void OnGameObjectDestroyed(IGameObject gameObject)
        {
            ICollisionComponent collisionComponent = gameObject.GetComponent<ICollisionComponent>();

            if (collisionComponent != null)
                collisionTree.Remove(collisionComponent);

            if (collisionEvents.ContainsKey(gameObject.GUID))
                collisionEvents.Remove(gameObject.GUID);

            currentCollisions[gameObject.GUID].Destroyed = true;
            currentCollisions.Remove(gameObject.GUID);
            listPool.CleanUp();

            gameObject.OnMove -= OnMove;
            gameObject.OnDestroyed -= OnGameObjectDestroyed;
        }

        private void RegisterObject(IGameObject theObject)
        {
            //Link ourselves to the OnMove and OnDestroyed events
            theObject.OnMove += OnMove;
            theObject.OnDestroyed += OnGameObjectDestroyed;

            collisionTree.Add(theObject.GetComponent<ICollisionComponent>());

            //Add an entry to the current collision dictionary and the collision events dictionary.
            currentCollisions.Add(theObject.GUID, listPool.New());
            currentCollisions[theObject.GUID].Source = theObject.GetComponent<ICollisionComponent>();
            collisionEvents.Add(theObject.GUID, null);
        }

        #endregion


        #region External Events

        private void NotifyCollision(ICollisionComponent source, ICollisionComponent collider)
        {
            var sourceGUID = source.Parent.GUID;
            var colliderGUID = collider.Parent.GUID;

            //Notify any listeners of the source that it has collided with something
            if (collisionEvents.ContainsKey(sourceGUID) && collisionEvents[sourceGUID] != null)
            {
                collisionEvents[sourceGUID](source, collider);
            }

            //Notify any listeners of the collider that is has collided with something
            if (collisionEvents.ContainsKey(colliderGUID) && collisionEvents[colliderGUID] != null)
            {
                collisionEvents[colliderGUID](collider, source);
            }
        }

        #endregion

        public override void Update(GameTime gameTime)
        {
            //Update any of the existing collisions to ensure that:
            // (a) The objects in question are still colliding, and,
            // (b) OnCollisionOccurred events are fired if they are still colliding.
            UpdateExistingCollisions();

            //Add any new collisions from movedLastFrame that do not already appear in the existing collisions.
            UpdateNewCollisions();

            //Clear the moved list
            movedLastFrame.Clear();
            
            base.Update(gameTime);
        }

        protected void UpdateExistingCollisions()
        {
            //Check any collision dictionaries that have elements and have them remove any objects no longer colliding with their source object.
            foreach (var collisionDictionary in currentCollisions.Values)
            {
                if (collisionDictionary.Count <= 0) continue;

                tempList.Clear();

                foreach (var oldCollision in collisionDictionary.Values)
                {
                    if (!collisionDictionary.Source.BoundingBox2D.Intersects(oldCollision.BoundingBox2D))
                    {
                        tempList.Add(oldCollision);
                    }
                    else
                    {
                        //Notify that a collision is still occurring.
                        NotifyCollision(collisionDictionary.Source, oldCollision);
                    }
                }

                //Remove all old collisions that are no longer colliding.
                foreach (var old in tempList)
                {
                    collisionDictionary.Remove(old.Parent.GUID);
                }
            }
        }


        protected void UpdateNewCollisions()
        {
            foreach (var moved in movedLastFrame)
            {
                tempList.Clear();
                collisionTree.GetObjects(moved.Value.BoundingBox2D, ref tempList);
                tempList.Remove(moved.Value);

                CullLayers(moved.Value, tempList);
                
                //Add any *new* collisions to the lists for each entity. 
                foreach (var collision in tempList)
                {
                    if (!currentCollisions[moved.Key].ContainsKey(collision.Parent.GUID))
                    {
                        currentCollisions[moved.Key].Add(collision.Parent.GUID, collision);
                        NotifyCollision(moved.Value, collision);
                    }
                    if (!currentCollisions[collision.Parent.GUID].ContainsKey(moved.Key))
                    {
                        currentCollisions[collision.Parent.GUID].Add(moved.Key, moved.Value); 
                        NotifyCollision(collision, moved.Value);
                    }
                } 
            }
        }

        protected void CullLayers(ICollisionComponent moved, List<ICollisionComponent> list)
        {
            for (int i = list.Count - 1; i >= 0 && i < list.Count; i++)
            {
                if (layerManager != null && !layerManager.LayersInteract(moved.Parent.Layer, list[i].Parent.Layer))
                {
                    list.RemoveRange(i, 1);
                }
            }
        }


        #region Collision Detection

        /// <summary>
        /// Detects all collisions against some source ICollisionComponent.
        /// </summary>
        /// <param name="source">The source ICollisionComponent that has moved and requires collision testing.</param>
        protected void DetectCollisions(ICollisionComponent source)
        {
            //Clear all existing collisions recorded.
            currentCollisions[source.Parent.GUID].Clear();
            tempList.Clear();

            //collisionTree.GetCollidingWith(source, ref tempList);

            //Run through each of the collisions and notify the impacted entities
            foreach (var collider in tempList)
            {
                //Add this to the current collisions for this source ICollisionComponent
                currentCollisions[source.Parent.GUID].Add(collider.Parent.GUID, collider);

                NotifyCollision(source, collider);
            }
        }

        #endregion
    }
}
